The Undo/Redo system in Vercengen is opt-in, and requires some initial architectural choices to be made for smoother integration into your program. Most Undo/Redo methods/classes are stored in the Delta Action Logging System (DALS).
Fundamentally, Undo/Redo is a tree system with multiple timelines. Each timeline is Git-like, with a Head state at [0], followed by a series of State Mutations from 1 to n. To activate Undo/Redo functionality, you must implement the following contract, or the linter will give you warnings and the ve.UndoRedo component will be non-functional.
DALS Contract
The DALS contract is documented in ./UF/js/dals/Timeline_state.js, which requires the following static functions to be fully implemented on your program's end:
- DALS.Timeline.loadState: function
- arg0_json: Object|string
- DALS.Timeline.saveState: function
- arg0_json: Object|string
In addition to serialising/deserialising the state, DALS requires you to parse all actions in a JSON-serialisable manner via DALS.Timeline.parseAction(). Actions should be appended via new DALS.Action(json).
For Actions to be grouped together, they must share the same .key in their pushed JSON value.
- DALS.Timeline.parseAction: function
- arg0_json: Object|string
Example (Naissance):
The following example implementation comes from Naissance HGIS, and is functionally complete:
//State mutation functions
{
DALS.Timeline.parseAction = function (arg0_json, arg1_do_not_push_action) {
//Convert from parameters
let json = (typeof arg0_json === "string") ? JSON.parse(arg0_json) : arg0_json;
let do_not_push_action = arg1_do_not_push_action;
//Initialise JSON
if (json.options === undefined) json.options = {};
if (json.value === undefined) json.value = [];
//Iterate over multi-value packet (MVP) and filter it down to superclass single-value packets (SVPs)
//console.log(json.value);
for (let i = 0; i < json.value.length; i++) {
if (json.value[i].type === "global") {
if (json.value[i].load_save)
DALS.Timeline.loadState(json.value[i].load_save);
if (json.value[i].set_date) {
main.date = json.value[i].set_date;
} else if (json.value[i].refresh_date === true) {
naissance.Geometry.instances.forEach((local_geometry) => local_geometry.draw());
}
continue;
}
if (json.value[i].type)
naissance[json.value[i].type].parseAction(json.value[i]);
}
//Save action to current timeline if needed
if (!do_not_push_action)
new DALS.Action(json);
//Force all UI_LeftbarHierarchy instances to .refresh()
UI_LeftbarHierarchy.refresh();
};
}
//State save/load functions
{
DALS.Timeline.loadState = function (arg0_json) { //[WIP] - Finish function body
//Convert from parameters
let json = (arg0_json) ? arg0_json : {};
if (typeof json === "string") json = JSON.parse(json);
//Clear map first, then naissance.Geometry.instances
for (let i = 0; i < naissance.Geometry.instances.length; i++)
naissance.Geometry.instances[i].remove();
scene.map_component.clear();
naissance.Feature.instances = [];
naissance.Geometry.instances = [];
console.log(`DALS.Timeline.loadState called.`);
//1. Handle naissance.Geometry classes
//Iterate over json to load in each class
Object.iterate(json, (local_key, local_value) => {
if (local_value.class_name && local_value.type === "geometry") {
let geometry_obj = new naissance[local_value.class_name]();
if (local_value.id) geometry_obj.id = local_value.id;
geometry_obj.history.fromJSON(local_value.history);
try {
if (geometry_obj.draw) geometry_obj.draw();
} catch (e) { console.warn(e); }
}
});
//2. Handle naissance.Feature classes
Object.iterate(json, (local_key, local_value) => {
if (local_value.class_name && local_value.type === "feature") {
let feature_obj = new naissance[local_value.class_name]();
if (local_value.id) feature_obj.id = local_value.id;
if (local_value.value) feature_obj.json = local_value.value;
}
});
for (let i = 0; i < naissance.Feature.instances.length; i++) {
let local_feature = naissance.Feature.instances[i];
local_feature.fromJSON(local_feature.json);
try {
if (local_feature.draw) local_feature.draw();
} catch (e) { console.warn(e); }
}
//3. Force all UI_LeftbarHierarchy instances to .refresh()
setTimeout(() => {
UI_LeftbarHierarchy.refresh();
}, 100);
//Reload cursor
main.layers.cursor_layer.addGeometry(main.brush.cursor)
};
DALS.Timeline.saveState = function () { //[WIP] - Finish function body for naissance.Feature
//Declare local instance variables
let json_obj = {};
//Iterate over all naissance.Geometry.instances and serialise them
for (let i = 0; i < naissance.Geometry.instances.length; i++) {
let local_geometry = naissance.Geometry.instances[i];
json_obj[local_geometry.id] = {
id: local_geometry.id,
class_name: local_geometry.class_name,
history: local_geometry.history.toJSON(),
type: "geometry"
};
}
//Iterate over all naissance.Feature.instances and serialise them
for (let i = 0; i < naissance.Feature.instances.length; i++) {
let local_feature = naissance.Feature.instances[i];
json_obj[local_feature.id] = {
id: local_feature.id,
class_name: local_feature.class_name,
type: "feature",
value: local_feature.toJSON()
};
}
//Return statement
return json_obj;
};
}